#!/usr/bin/env python

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##
# Whisk Admin command line interface
##

import argparse
import json
import os
import random
import re
from subprocess import Popen, PIPE, STDOUT
import string
import sys
import traceback
import uuid
import wskprop
if sys.version_info.major >= 3:
    from urllib.parse import quote_plus
else:
    from urllib import quote_plus
try:
    import argcomplete
except ImportError:
    argcomplete = False
from wskutil import request

DB_PROTOCOL = 'DB_PROTOCOL'
DB_HOST     = 'DB_HOST'
DB_PORT     = 'DB_PORT'
DB_USERNAME = 'DB_USERNAME'
DB_PASSWORD = 'DB_PASSWORD'

DB_WHISK_AUTHS   = 'DB_WHISK_AUTHS'
DB_WHISK_ACTIONS = 'DB_WHISK_ACTIONS'
DB_WHISK_ACTIVATIONS = 'DB_WHISK_ACTIVATIONS'

LOGS_DIR = 'WHISK_LOGS_DIR'

# SCRIPT_DIR is going to be traversing all links and point to tools/cli/wsk
CLI_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))
# ROOT_DIR is the repository root
ROOT_DIR = os.path.join(os.path.join(CLI_DIR, os.pardir), os.pardir)

def main():
    requiredprops = [
        DB_PROTOCOL, DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD,
        DB_WHISK_AUTHS, DB_WHISK_ACTIONS, DB_WHISK_ACTIVATIONS,
        LOGS_DIR ]
    whiskprops = wskprop.importPropsIfAvailable(wskprop.propfile(ROOT_DIR))
    (valid, props, deferredInfo) = wskprop.checkRequiredProperties(requiredprops, whiskprops)

    exitCode = 0 if valid else 2
    if valid:
        try:
            args = parseArgs()
            if (args.verbose):
                print(deferredInfo)
            exitCode = {
              'user' : userCmd,
              'db'   : dbCmd,
              'syslog' : syslogCmd,
              'limits': limitsCmd
            }[args.cmd](args, props)
        except Exception as e:
            print('Exception: ', e)
            print('Informative: ', deferredInfo)
            traceback.print_exc()
            exitCode = 1
    sys.exit(exitCode)

def str_to_bool(value):
    if value.lower() in ("yes", "true"):
        return True
    elif value.lower() in ("no", "false"):
        return False
    else:
        raise argparse.ArgumentTypeError("%s is not a valid boolean." % value)

def parseArgs():
    parser = argparse.ArgumentParser(description='OpenWhisk admin command line tool')
    parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')
    subparsers = parser.add_subparsers(title='available commands', dest='cmd')
    subparsers.required = True

    propmenu = subparsers.add_parser('user', help='manage users')
    propmenu.add_argument('-w', '--view', help='the subject view to query', default='subjects.v2.0.0')
    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
    subparser.required = True

    subcmd = subparser.add_parser('create', help='create a user and show authorization key')
    subcmd.add_argument('subject', help='the subject to create')
    subcmd.add_argument('-u', '--auth', help='the uuid:key to initialize the subject authorization key with')
    subcmd.add_argument('-ns', '--namespace', help='create key for given namespace instead (defaults to subject id')
    subcmd.add_argument('-r', '--revoke', help='revoke existing key and create a new one', action='store_true')
    subcmd.add_argument('-g', '--genonly', help='generate a uuid and key but do not store them in the database', action='store_true')
    subcmd.add_argument('-s', '--silent', help='do not should the new key on the console', action='store_true')

    subcmd = subparser.add_parser('delete', help='delete a user')
    subcmd.add_argument('subject', help='the subject to delete')
    subcmd.add_argument('-ns', '--namespace', help='delete key for given namespace only')

    subcmd = subparser.add_parser('get', help='get authorization key for user')
    subcmd.add_argument('subject', help='the subject to get key for')
    subcmd.add_argument('-ns', '--namespace', help='the namespace to get the key for, defaults to subject id')
    subcmd.add_argument('-a', '--all', help='list all namespaces and their keys', action='store_true')

    subcmd = subparser.add_parser('whois', help='identify user from an authorization key')
    subcmd.add_argument('authkey', help='the credentials to look up')

    subcmd = subparser.add_parser('block', help='block one or more users')
    subcmd.add_argument('subjects', nargs='+', help='one or more users to block')

    subcmd = subparser.add_parser('unblock', help='unblock one or more users')
    subcmd.add_argument('subjects', nargs='+', help='one or more users to unblock')

    subcmd = subparser.add_parser('list', help='list authorization keys associated with a namespace')
    subcmd.add_argument('namespace', help='the namespace to lookup')
    subcmd.add_argument('-p', '--pick', metavar='N', help='show no more than N identities', type=int, default=1)
    subcmd.add_argument('-a', '--all', help='show all identities', action='store_true')
    subcmd.add_argument('-k', '--key', help='show only the keys', action='store_true')

    propmenu = subparsers.add_parser('limits', help='manage namespace-specific limits')
    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
    subparser.required = True

    subcmd = subparser.add_parser('set', help='set limits for a given namespace')
    subcmd.add_argument('namespace', help='the namespace to set limits for')
    subcmd.add_argument('--invocationsPerMinute', help='invocations per minute allowed', type=int)
    subcmd.add_argument('--firesPerMinute', help='trigger fires per minute allowed', type=int)
    subcmd.add_argument('--concurrentInvocations', help='concurrent invocations allowed for this namespace', type=int)
    subcmd.add_argument('--allowedKinds', help='list of runtime kinds allowed in this namespace', nargs='+', type=str)
    subcmd.add_argument('--storeActivations', help='enable or disable storing of activations to datastore for this namespace', default=None, type=str_to_bool)
    subcmd.add_argument('--minActionMemory', help='minimum action memory size for this namespace', default=None, type=int)
    subcmd.add_argument('--maxActionMemory', help='maximum action memory size for this namespace', default=None, type=int)
    subcmd.add_argument('--minActionLogs', help='minimum activation log size for this namespace', default=None, type=int)
    subcmd.add_argument('--maxActionLogs', help='maximum activation log size for this namespace', default=None, type=int)
    subcmd.add_argument('--minActionTimeout', help='minimum action time limit for this namespace', default=None, type=int)
    subcmd.add_argument('--maxActionTimeout', help='maximum action time limit for this namespace', default=None, type=int)
    subcmd.add_argument('--minActionConcurrency', help='minimum action concurrency limit for this namespace', default=None, type=int)
    subcmd.add_argument('--maxActionConcurrency', help='maximum action concurrency limit for this namespace', default=None, type=int)
    subcmd.add_argument('--maxParameterSize', help='maximum parameter size for this namespace', default=None, type=str)
    subcmd.add_argument('--maxPayloadSize', help='maximum payload size for this namespace', default=None, type=str)
    subcmd.add_argument('--truncationSize', help='activation truncation size for this namespace', default=None, type=str)

    subcmd = subparser.add_parser('get', help='get limits for a given namespace (if none exist, system defaults apply)')
    subcmd.add_argument('namespace', help='the namespace to get limits for')

    subcmd = subparser.add_parser('delete', help='delete limits for a given namespace (system defaults apply)')
    subcmd.add_argument('namespace', help='the namespace to delete limits for')

    propmenu = subparsers.add_parser('db', help='work with dbs')
    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
    subparser.required = True

    subcmd = subparser.add_parser('get', help='get contents of database')
    subcmd.add_argument('database', help='the database name')
    subcmd.add_argument('-w', '--view', help='the view in the database to query')
    subcmd.add_argument('--docs', help='include document contents', action='store_true')

    propmenu = subparsers.add_parser('syslog', help='work with system logs')
    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
    subparser.required = True

    subcmd = subparser.add_parser('get', help='get logs')
    subcmd.add_argument('components', help='components, one or more of [controllerN, schedulerN, invokerN] where N is the instance', nargs='*', default=['controller0', 'scheduler0', 'invoker0'])
    subcmd.add_argument('-t', '--tid', help='retrieve logs for the transaction id')
    subcmd.add_argument('-g', '--grep', help='retrieve logs that match grep expression')

    if argcomplete:
        argcomplete.autocomplete(parser)
    return parser.parse_args()

def userCmd(args, props):
    if args.subcmd == 'create':
        return createUserCmd(args, props)
    elif args.subcmd == 'delete':
        return deleteUserCmd(args, props)
    elif args.subcmd == 'get':
        return getUserCmd(args, props)
    elif args.subcmd == 'whois':
        return whoisUserCmd(args, props)
    elif args.subcmd == 'list':
        return listUserCmd(args, props)
    elif args.subcmd == 'block':
        return blockUserCmd(args, props)
    elif args.subcmd == 'unblock':
        return unblockUserCmd(args, props)
    else:
        print('unknown command')
        return 2

def dbCmd(args, props):
    if args.subcmd == 'get':
        return getDbCmd(args, props)
    else:
        print('unknown command')
        return 2

def syslogCmd(args, props):
    if args.subcmd == 'get':
        return getLogsCmd(args, props)
    else:
        print('unknown command')
        return 2

def limitsCmd(args, props):
    if args.subcmd == 'set':
        return setLimitsCmd(args, props)
    elif args.subcmd == 'get':
        return getLimitsCmd(args, props)
    elif args.subcmd == 'delete':
        return deleteLimitsCmd(args, props)
    else:
        print('unknown command')
        return 2

def createUserCmd(args, props):
    subject = args.subject.strip()
    if len(subject) < 5:
        print('Subject name must be at least 5 characters')
        return 2

    if args.namespace and args.namespace.strip() == '':
        print('Namespace must not be empty')
        return 2
    else:
        desiredNamespace = subject if not args.namespace else args.namespace.strip()

    if args.auth:
        try:
            parts = args.auth.split(':')
            try:
                uid = str(uuid.UUID(parts[0], version = 4))
            except ValueError:
                print('authorization id is not a valid UUID')
                return 2

            key = parts[1]
            if len(key) < 64:
                print('authorization key must be at least 64 characters long')
                return 2
        except Exception as e:
            print('failed to determine authorization id and key: %s' % e)
            return 2
    else:
        uid = str(uuid.uuid4())
        key = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))

    if args.genonly:
        print('%s:%s' % (uid, key))
        return 0

    (doc, res) = getDocumentFromDb(props, args.subject, args.verbose)
    if doc is None:
        doc = {
            '_id': subject,
            'subject': subject,
            'namespaces': [
                {
                    'name': desiredNamespace,
                    'uuid': uid,
                    'key': key
                }
            ]
        }
    else:
        if not doc.get('blocked'):
            namespaces = [ns for ns in doc['namespaces'] if ns['name'] == desiredNamespace]
            if len(namespaces) == 0:
                doc['namespaces'].append({
                    'name': desiredNamespace,
                    'uuid': uid,
                    'key': key
                })
            elif args.revoke:
                if len(namespaces) == 1:
                    namespaces[0]['uuid'] = uid
                    namespaces[0]['key'] = key
                else:
                    print('Namespace is not unique')
                    return 1
            else:
                print('Namespace already exists')
                return 1
        else:
            print('The subject you want to edit is blocked')
            return 1

    res = insertIntoDatabase(props, doc, args.verbose)
    if res.status in [201, 202]:
        if not args.silent:
            print('%s:%s' % (uid, key))
    else:
        print('Failed to create subject (%s)' % res.read().strip())
        return 1

def getUserCmd(args, props):
    (doc, res) = getDocumentFromDb(props, args.subject, args.verbose)

    if doc is not None:
        if args.all is True:
            # tabulate name of each space and its key
            for ns in doc['namespaces']:
                print('%s\t%s:%s' % (ns['name'], ns['uuid'], ns['key']))
            return 0
        else:
          # if requesting key for specific namespace, report only that key;
          # use default namespace if no namespace provided
          namespaceName = args.namespace if args.namespace is not None else args.subject
          namespaces = [ns for ns in doc['namespaces'] if ns['name'] == namespaceName]
          if len(namespaces) == 1:
              ns = namespaces[0]
              print('%s:%s' % (ns['uuid'], ns['key']))
              return 0
          else:
              print('namespace "%s" not found for "%s"' % (namespaceName, args.subject))
              return 1
    else:
        print('Failed to get subject (%s)' % res.read().strip())
        return 1

def listUserCmd(args, props):
    (nslist, res) = getIdentitiesFromNamespace(args, props)

    if args.pick < 1:
        print('pick at least 1 identity to show')
        return 2

    if nslist is not None:
        nslist = nslist if args.all is True else nslist[:args.pick]
        if len(nslist) > 0:
            for p in nslist:
                print('%s:%s%s' % (p['uuid'], p['key'], "\t%s" % p['subject'] if not args.key else ""))
            return 0
        else:
            print('no identities found for namespace "%s"' % args.namespace)
            return 0
    else:
        print('Failed to get namespace key (%s)' % res.read().strip())
        return 1

def getDocumentFromDb(props, doc, verbose):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'database': database,
        'subject' : doc
    }

    headers = {
        'Content-Type': 'application/json',
    }

    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=verbose)
    if res.status == 200:
        doc = json.loads(res.read())
        return (doc, res)
    else:
        return (None, res)

def getIdentitiesFromNamespace(args, props):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/%(view)s/_view/identities?key=["%(ns)s"]' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'username': username,
        'database': database,
        'view'    : args.view,
        'ns'      : args.namespace
    }

    headers = {
        'Content-Type': 'application/json',
    }

    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
    nslist = None
    if res.status == 200:
        doc = json.loads(res.read())
        nslist = []
        if 'rows' in doc and len(doc['rows']) > 0:
            for row in doc['rows']:
                if 'id' in row:
                    nslist.append({"subject": row["id"], "uuid": row['value']['uuid'], "key": row['value']['key']})
    return (nslist, res)

def deleteUserCmd(args, props):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    if args.subject.strip() == '':
        print('Subject must not be empty')
        return 2

    if args.namespace and args.namespace.strip() == '':
        print('Namespace must not be empty')
        return 2

    (prev, res) = getDocumentFromDb(props, args.subject, args.verbose)
    if prev is None:
        print('Failed to delete subject (%s)' % res.read().strip())
        return 1

    if not args.namespace:
        url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s?rev=%(rev)s' % {
            'protocol': protocol,
            'host'    : host,
            'port'    : port,
            'database': database,
            'subject' : args.subject.strip(),
            'rev'     : prev['_rev']
        }

        headers = {
            'Content-Type': 'application/json',
        }

        res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
        if res.status in [200, 202]:
            print('Subject deleted')
        else:
            print('Failed to delete subject (%s)' % res.read().strip())
            return 1
    else:
        namespaceToDelete = args.namespace.strip()
        namespaces = [ns for ns in prev['namespaces'] if ns['name'] != namespaceToDelete]
        if len(prev['namespaces']) == len(namespaces):
            print('Namespace "%s" does not exist for "%s"' % (namespaceToDelete, prev['_id']))
            return 1
        else:
            prev['namespaces'] = namespaces
            res = insertIntoDatabase(props, prev, args.verbose)
            if res.status in [201, 202]:
                print('Namespace deleted')
            else:
                print('Failed to remove namespace (%s)' % res.read().strip())
                return 1

def whoisUserCmd(args, props):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    authParts = args.authkey.split(':')
    uuid      = authParts[0]
    key       = authParts[1]

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/%(view)s/_view/identities?key=["%(uuid)s","%(key)s"]' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'username': username,
        'database': database,
        'view'    : args.view,
        'uuid'    : uuid,
        'key'     : key
    }

    headers = {
        'Content-Type': 'application/json',
    }

    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
    if res.status == 200:
        doc = json.loads(res.read())
        if 'rows' in doc and len(doc['rows']) > 0:
            for row in doc['rows']:
                if 'id' in row:
                    print('subject: %s' % row['id'])
                    print('namespace: %s' % row['value']['namespace'])
        else:
            print('Subject id is not recognized')
        return 0
    print('Failed to get subject (%s)' % res.read().strip())
    return 1

def blockUserCmd(args, props):
    failed = 0
    for subject in args.subjects:
        subject = subject.strip()
        if len(subject) > 0:
            (doc, res) = getDocumentFromDb(props, subject, args.verbose)

            if doc is not None:
                doc['blocked'] = True
                insertRes = insertIntoDatabase(props, doc, args.verbose)
                if insertRes.status in [201, 202]:
                    print('"%s" blocked successfully' % subject)
                else:
                    print('Failed to block "%s" (%s)' % (subject, res.read().strip()))
                    failed += 1
            else:
                print('Failed to block "%s" (%s)' % (subject, res.read().strip()))
                failed += 1
    return failed

def unblockUserCmd(args, props):
    failed = 0
    for subject in args.subjects:
        subject = subject.strip()
        if len(subject) > 0:
            (doc, res) = getDocumentFromDb(props, subject, args.verbose)

            if doc is not None:
                doc['blocked'] = False
                insertRes = insertIntoDatabase(props, doc, args.verbose)
                if insertRes.status in [201, 202]:
                    print('"%s" unblocked successfully' % subject)
                else:
                    print('Failed to unblock "%s" (%s)' % (subject, res.read().strip()))
                    failed += 1
            else:
                print('Failed to unblock "%s" (%s)' % (subject, res.read().strip()))
                failed += 1
    return failed

def setLimitsCmd(args, props):
    argsDict = vars(args)
    docId = args.namespace + "/limits"
    (dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)
    doc = dbDoc or {'_id': docId}

    limits = [
        'invocationsPerMinute',
        'firesPerMinute',
        'concurrentInvocations',
        'allowedKinds',
        'storeActivations',
        'minActionMemory',
        'maxActionMemory',
        'minActionLogs',
        'maxActionLogs',
        'minActionTimeout',
        'maxActionTimeout',
        'minActionConcurrency',
        'maxActionConcurrency',
        'maxParameterSize',
        'maxPayloadSize',
        'truncationSize'
        ]
    for limit in limits:
        givenLimit = argsDict.get(limit)
        toSet = givenLimit if givenLimit != None else doc.get(limit)
        if toSet != None:
            doc[limit] = toSet

    res = insertIntoDatabase(props, doc, args.verbose)
    if res.status in [201, 202]:
        print('Limits successfully set for "%s"' % args.namespace)
    else:
        print('Failed to set limits (%s)' % res.read().strip())
        return 1

def getLimitsCmd(args, props):
    docId = args.namespace + "/limits"
    (dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)

    if dbDoc is not None:
        limits = [
            'invocationsPerMinute',
            'firesPerMinute',
            'concurrentInvocations',
            'allowedKinds',
            'storeActivations',
            'minActionMemory',
            'maxActionMemory',
            'minActionLogs',
            'maxActionLogs',
            'minActionTimeout',
            'maxActionTimeout',
            'minActionConcurrency',
            'maxActionConcurrency',
            'maxParameterSize',
            'maxPayloadSize',
            'truncationSize'
            ]
        for limit in limits:
            givenLimit = dbDoc.get(limit)
            if givenLimit != None:
                print('%s = %s' % (limit, givenLimit))
    else:
        error = json.loads(res.read())
        if error['reason'] == 'missing' or error['reason'] == 'deleted':
            print('No limits found, default system limits apply')
            return 0
        else:
            print('Failed to get limits (%s)' % res.read().strip())
        return 1

def deleteLimitsCmd(args, props):
    docId = quote_plus(args.namespace + "/limits")
    (dbDoc, res) = getDocumentFromDb(props, docId, args.verbose)

    if dbDoc is None:
        print('Failed to delete limits (%s)' % res.read().strip())
        return 1

    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(docid)s?rev=%(rev)s' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'database': database,
        'docid'   : docId,
        'rev'     : dbDoc['_rev']
    }

    headers = {
        'Content-Type': 'application/json',
    }

    res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
    if res.status in [200, 202]:
        print('Limits deleted')
    else:
        print('Failed to delete limits (%s)' % res.read().strip())
        return 1

def getDbCmd(args, props):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]

    if args.database == 'subjects':
        database = props[DB_WHISK_AUTHS]
    elif args.database == 'whisks':
        database = props[DB_WHISK_ACTIONS]
    elif args.database == 'activations':
        database = props[DB_WHISK_ACTIVATIONS]
    else:
        database = args.database

    if args.view:
        try:
            parts = args.view.split('/')
            designdoc = parts[0]
            viewname  = parts[1]
        except:
            print('view name "%s" is not formatted correctly, should be design/view' % args.view)
            return 2

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s%(design)s/%(index)s?reduce=false&include_docs=%(docs)s' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'database': database,
        'design'  : '/_design/' + designdoc +'/_view' if args.view else '',
        'index'   : viewname if args.view else '_all_docs',
        'docs'    : 'true' if args.docs else 'false'
    }

    headers = {
        'Content-Type': 'application/json',
    }

    print('getting contents for %s (%s)' % (database, args.view if args.view else 'primary index'))
    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
    if res.status == 200:
        table = json.loads(res.read())
        print(json.dumps(table, sort_keys=True, indent=4, separators=(',', ': ')))
        return 0
    print('Failed to get database (%s)' % res.read().strip())
    return 1

def insertIntoDatabase(props, doc, verbose = False):
    protocol = props[DB_PROTOCOL]
    host     = props[DB_HOST]
    port     = props[DB_PORT]
    username = props[DB_USERNAME]
    password = props[DB_PASSWORD]
    database = props[DB_WHISK_AUTHS]

    url = '%(protocol)s://%(host)s:%(port)s/%(database)s' % {
        'protocol': protocol,
        'host'    : host,
        'port'    : port,
        'database': database
    }
    body = json.dumps(doc)
    headers = {
        'Content-Type': 'application/json',
    }

    res = request('POST', url, body, headers, auth='%s:%s' % (username, password), verbose=verbose)
    return res

def getLogsCmd(args, props):
    def getComponentLogs(component):
        path = '%s/%s/%s_logs.log' % (props[LOGS_DIR], component, component)
        if args.tid:
            cmd = 'grep "\[#tid_%s\]" %s' % (args.tid, path)
        elif args.grep:
            cmd = 'grep "%s" %s' % (args.grep, path)
        else:
            cmd = 'cat %s' % path
        (output, error) = shell(cmd, verbose = args.verbose)

        if output:
            return output.decode('utf-8')
        if error:
            sys.stderr.write(error)
        return ''

    logs = map(getComponentLogs, args.components)
    joined = ''.join(logs)

    if joined:
        output = joined.strip()
        parts = output.split('\n')
        filter = [p for p in parts if p != '']
        date = map(extractDate, filter)
        keyed = zip(date, parts)
        sort = sorted(keyed, key=lambda t: t[1])
        msgs = list(unzip(sort))[1]
        print('\n'.join(msgs))
    return 0

def shell(cmd, data=None, verbose=False):
    if verbose:
        print(cmd)
    if input:
        p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, stdin=PIPE)
        out, err = p.communicate(input=data)
    else:
        out, err = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT)
    p.wait()
    return (out, err)

def unzip(iterable):
    return zip(*iterable)

def extractDate(line):
    matches = re.search(r'\d{4}-[01]{1}\d{1}-[0-3]{1}\d{1}T[0-2]{1}\d{1}:[0-6]{1}\d{1}:[0-6]{1}\d{1}.\d{3}Z', line)
    if matches is not None:
        date = matches.group(0)
        return date
    else:
        return None

if __name__ == '__main__':
    main()
